﻿using System.Net;

namespace PicView.Core.Http;

public sealed class HttpClientDownloadWithProgress : IDisposable
{
    public delegate void ProgressChangedHandler(long? totalFileSize, long? totalBytesDownloaded,
        double? progressPercentage);

    private readonly string _downloadUrl;
    private readonly string _destinationFilePath;
    private readonly HttpClient _httpClient;
    private bool _disposed;

    public event ProgressChangedHandler? ProgressChanged;
    
    /// <summary>
    /// Initializes a new instance of HttpClientDownloadWithProgress
    /// </summary>
    /// <param name="downloadUrl">URL to download from</param>
    /// <param name="destinationFilePath">Where to save the downloaded file</param>
    /// <param name="httpClient">Optional custom HttpClient instance</param>
    public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, HttpClient? httpClient = null)
    {
        _downloadUrl = downloadUrl ?? throw new ArgumentNullException(nameof(downloadUrl));
        _destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
        _httpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromHours(1) };
    }

    /// <summary>
    /// Starts downloading the file asynchronously
    /// </summary>
    /// <param name="cancellationToken">Token to cancel the download</param>
    /// <returns>Task representing the download operation</returns>
    /// <exception cref="HttpRequestException">Thrown when the download fails</exception>
    public async Task StartDownloadAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            using var response = await _httpClient.GetAsync(
                _downloadUrl, 
                HttpCompletionOption.ResponseHeadersRead,
                cancellationToken).ConfigureAwait(false);
                
            await DownloadFileFromHttpResponseMessage(response, cancellationToken).ConfigureAwait(false);
        }
        catch (TaskCanceledException)
        {
            // Clean up partial downloads
            if (File.Exists(_destinationFilePath))
            {
                try { File.Delete(_destinationFilePath); } catch { /* Ignore cleanup failures */ }
            }
            throw;
        }
        catch (Exception ex)
        {
            throw new HttpRequestException($"Failed to download file from {_downloadUrl}: {ex.Message}", ex);
        }
    }

    private async Task DownloadFileFromHttpResponseMessage(
        HttpResponseMessage response, 
        CancellationToken cancellationToken)
    {
        if (!response.IsSuccessStatusCode)
        {
            if (response.StatusCode == HttpStatusCode.NotFound)
                throw new FileNotFoundException($"The requested file at {_downloadUrl} was not found.", _downloadUrl);
                
            throw new HttpRequestException(
                $"Download failed with status code {response.StatusCode}: {response.ReasonPhrase}");
        }
        
        var totalBytes = response.Content.Headers.ContentLength;
        await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        await ProcessContentStream(totalBytes, contentStream, cancellationToken).ConfigureAwait(false);
    }

    private async Task ProcessContentStream(
        long? totalDownloadSize, 
        Stream contentStream,
        CancellationToken cancellationToken)
    {
        const int bufferSize = 81920; // Larger buffer for better performance
        var buffer = new byte[bufferSize];
        var totalBytesRead = 0L;
        
        // Ensure the directory exists
        var directory = Path.GetDirectoryName(_destinationFilePath);
        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        await using var fileStream = new FileStream(
            _destinationFilePath, 
            FileMode.Create, 
            FileAccess.Write,
            FileShare.None, 
            bufferSize, 
            true);
            
        int bytesRead;
        
        do
        {
            bytesRead = await contentStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);

            if (bytesRead <= 0)
            {
                continue;
            }

            await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
            totalBytesRead += bytesRead;

            if (totalDownloadSize.HasValue)
            {
                var progressPercentage = (double)totalBytesRead / totalDownloadSize.Value * 100;
                OnProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
            }
            else
            {
                // If we don't know the total size, just report bytes downloaded
                OnProgressChanged(null, totalBytesRead, null);
            }
        } while (bytesRead > 0 && !cancellationToken.IsCancellationRequested);
        
        // Flush to ensure all data is written
        await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
        
        if (cancellationToken.IsCancellationRequested)
        {
            throw new TaskCanceledException("Download was canceled");
        }
    }

    private void OnProgressChanged(long? totalDownloadSize, long totalBytesRead, double? progressPercentage)
    {
        ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
    }

    #region IDisposable

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            _httpClient.Dispose();
        }

        _disposed = true;
    }

    ~HttpClientDownloadWithProgress()
    {
        Dispose(false);
    }

    #endregion
}